<?php

/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */

/**
 * This file contains the PDORM class and some helper functions for PDORM.
 *
 * This file contains the following class and helper functions:
 *
 * <ul>
 * <li>PDORM<li>
 * <li>PDORM_assert_valid_keys</li>
 * <li>PDORM_is_assoc</li>
 * <li>PDORM_lcfirst</li>
 * <li>PDORM_camel2uscore</li>
 * <li>PDORM_uscore2camel</li>
 * </ul>
 *
 * PHP version 5
 *
 * This file is part of PEAR::PDORM - A database object relational management
 * layer for PHP 5 built using PDO. PHP version 5.1+ is recommended.
 *
 * PEAR::PDORM is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option)
 * any later version.
 *
 * PEAR::PDORM is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with PEAR::PDORM. If not, see <http://www.gnu.org/licenses/>.
 *
 * @package     PDORM
 * @category    Object-relational Management
 * @author      Michael J. I. Jackson <mjijackson@gmail.com>
 * @copyright   2007 Michael J. I. Jackson
 * @license     http://www.gnu.org/licenses/lgpl-3.0.txt GNU LGPL 3.0
 * @version     SVN: $Id: PDORM.php 40 2007-12-11 22:54:47Z mjijackson $
 * @link        http://pear.php.net/packages/PDORM
 */

/**
 * Require PDODB
 */
require_once 'PDODB.php';

/**
 * Require PDORM_Exception
 */
require_once str_replace('.php', '', __FILE__) . DIRECTORY_SEPARATOR . 'Exception.php';

/**
 * Require PDORM_Inflector
 */
require_once str_replace('.php', '', __FILE__) . DIRECTORY_SEPARATOR . 'Inflector.php';

/**
 * Require PDORM_Mapper
 */
require_once str_replace('.php', '', __FILE__) . DIRECTORY_SEPARATOR . 'Mapper.php';

/**
 * Require PDORM_Object
 */
require_once str_replace('.php', '', __FILE__) . DIRECTORY_SEPARATOR . 'Object.php';

/**
 * Require PDORM_Collection
 */
require_once str_replace('.php', '', __FILE__) . DIRECTORY_SEPARATOR . 'Collection.php';

/**
 * This class serves a few different purposes. They are:
 *
 * - An identity map for domain objects fetched using a {@link PDORM_Mapper mapper}.
 *   This means that this class is responsible for keeping a cache of all
 *   fetched objects and ensuring that the SAME objects are fetched on subsequent
 *   requests for them.
 * - A Unit of Work helper. When {@link performOperations()} is called, this
 *   class will try to {@link PDORM_Object::save() save} all domain objects in
 *   the cache. PDORM domain objects are smart enough to keep track of their own
 *   state, so they won't actually be saved unless they are
 *   {@link PDORM_Object::$_dirty dirty}. This behavior can be entirely automated
 *   using the {@link autosave} feature. When {@link enableAutosave() enabled},
 *   this feature will call {@link performOperations()} on __destruct.
 * - A database connection manager. This is accomplished by means of connection
 *   {@link PDORM_Mapper::$conn_alias aliases} that mappers can use to specify
 *   which database connection they will use when executing queries. Using the
 *   same alias ensures the same database connection will be used every time.
 *   Connections may be generated using {@link addConnection()} and retrieved
 *   using {@link getConnection()}.
 * - A domain mapper generator. This class provides a convenient
 *   {@link mapperFactory() factory method} for retrieving domain mapper
 *   instances for a given domain class.
 * - This class also keeps the domain {@link $mapper_prefix mapper} prefix. This
 *   prefix may be changed if your domain mapper class naming convention does
 *   not follow the default 'Mapper_*'.
 *
 * @package     PDORM
 * @category    Object-relational Management
 * @author      Michael J. I. Jackson <mjijackson@gmail.com>
 * @final
 */
final class PDORM
{

    /**
     * The PDORM API version number.
     *
     * @var     string
     * @access  private
     * @static
     */
    private static $api_version = '0.1';

    /**
     * Contains all connection instances.
     *
     * @var     array
     * @access  private
     */
    private $connections = array();

    /**
     * Contains all objects in the domain.
     *
     * @var     array
     * @access  private
     */
    private $objects = array();

    /**
     * Determines whether this class will automatically save all objects
     * on {@link __destruct()}.
     *
     * @var     bool
     * @access  private
     */
    private $autosave = false;

    /**
     * The prefix to use for all domain mapper class names. Defaults to
     * 'Mapper_'.
     *
     * @var     string
     * @access  private
     */
    private $mapper_prefix = 'Mapper_';

    /**
     * Constructor.
     *
     * @access  private
     */
    private function __construct() {}

    /**
     * Called automatically upon destruction of the object cache. If the
     * {@link $autosave autosave} feature is enabled, this function will
     * automatically save all 'dirty' domain objects.
     *
     * @return  void
     * @access  public
     */
    public function __destruct()
    {
        if ($this->autosave) {
            $this->performOperations();
        }
    }

    /**
     * Gets the instance of the PDORM class.
     *
     * @return  PDORM    The PDORM singleton object
     * @access  public
     * @static
     */
    public static function getInstance()
    {
        static $inst;
        if (!$inst) {
            $inst = new self();
        }
        return $inst;
    }

    /**
     * Gets the PDORM API version number.
     *
     * @return  string      The PDORM API version number
     * @access  public
     * @static
     */
    public static function apiVersion()
    {
        return self::$api_version;
    }

    /**
     * Enables the {@link $autosave autosave} feature.
     *
     * @return  void
     * @access  public
     * @static
     */
    public static function enableAutosave()
    {
        $inst = self::getInstance();
        $inst->autosave = true;
    }

    /**
     * Adds an object to the cache.
     *
     * @param   PDORM_Object     $obj   The object to put in the cache
     * @return  void
     * @access  public
     */
    public static function add(PDORM_Object $obj)
    {
        $inst = self::getInstance();
        $key = $inst->globalKey($obj);
        if (!isset($inst->objects[$key])) {
            $inst->objects[$key] = $obj;
        }
    }

    /**
     * Gets an object from the cache.
     *
     * @param   string  $class_name     The class name of the object
     * @param   int     $id             The id of an object
     * @return  PDORM_Object            The corresponding domain object if it
     *                                  resides in the cache, null otherwise
     * @access  public
     * @static
     */
    public static function get($class_name, $id = null)
    {
        $inst = self::getInstance();
        $key = "$class_name.$id";
        return isset($inst->objects[$key]) ? $inst->objects[$key] : null;
    }

    /**
     * Gets a global key for the specified object.
     *
     * @param   PDORM_Object    $obj            The object to generate the key for
     * @return  string                          A global key for the object
     * @access  private
     */
    private function globalKey(PDORM_Object $obj)
    {
        return get_class($obj) . '.' . $obj->getId();
    }

    /**
     * Performs all pending operations on domain objects in the cache.
     *
     * @return  void
     * @access  public
     */
    public function performOperations()
    {
        foreach ($this->objects as $obj) {
            $obj->save();
        }
    }

    /**
     * Gets an instance of the mapper class for a given domain class. If the
     * mapper class is not already loaded, this function will attempt to use
     * {@link http://php.net/__autoload __autoload} to automatically load the
     * appropriate mapper.
     *
     * @param   string          $domain_cls     The name of the domain class
     * @return  PDORM_Mapper                    The mapper class singleton
     * @throws  PDORM_ClassNotFoundException    If the mapper class cannot be
     *                                          found
     * @access  public
     * @static
     */
    public static function mapperFactory($domain_cls)
    {
        $mapper_cls = PDORM::mapperPrefix() . $domain_cls;
        if (!class_exists($mapper_cls, true)) {
            throw new PDORM_ClassNotFoundException($mapper_cls);
        }

        return call_user_func(array($mapper_cls, 'getInstance'));
    }

    /**
     * Gets/sets the {@link $mapper_prefix prefix} to use for all domain
     * mapper class names. If $prefix is a string, it will be set. In any case,
     * the current prefix in use will be returned.
     *
     * @param   string  $prefix         The prefix to use for all mappers
     * @return  string                  The mapper prefix in use
     * @access  public
     * @static
     */
    public static function mapperPrefix($prefix = null)
    {
        $inst = self::getInstance();
        if (is_string($prefix)) {
            $inst->mapper_prefix = $prefix;
        }

        return $inst->mapper_prefix;
    }

    /**
     * Adds a database connection to the domain connection pool with the given
     * alias. This connection may later be retrieved using
     * {@link getConnection()}. Implicitly throws an exception if the connection
     * fails. May be used with a lazy connection:
     *
     * <code>
     * PDORM::addConnection('default', $dsn, $user, $pass);
     * ...
     * $conn = PDORM::getConnection('default');
     * </code>
     *
     * or with a PDO instance:
     *
     * <code>
     * PDORM::addConnection('default', new PDO($dsn, $user, $pass), $options);
     * </code>
     *
     * Note: In the second example, when the second parameter is a PDO instance,
     * the third parameter acts as the $options array and all other parameters
     * are ignored. See {@link PDODB::factory()} for more information.
     *
     * @param   string  $alias      The connection alias to use
     * @param   mixed   $dsn        The PDO DSN to use for the connection (lazy)
     *                              or a PDO instance
     * @param   mixed   $user       The database user name or an $options array
     *                              if $dsn is a PDO instance
     * @param   string  $pass       The database password
     * @param   array   $options    An array of driver-specific options to use
     * @return  int                 The connection's global instance index
     * @access  public
     * @throws  PDORM_Exception     If creation of the connection instance fails
     * @static
     */
    public static function addConnection($alias, $dsn, $user = null,
        $pass = null, $options = array())
    {
        $inst = self::getInstance();

        if ($dsn instanceof PDO) {
            $options = $user;
            $user = null;
        }

        $db = PDODB::factory($dsn, $user, $pass, $options);
        $inst->connections[$alias] = $db->getId();

        return $inst->connections[$alias];
    }

    /**
     * Gets the database connection with the given alias.
     *
     * @param   mixed           $alias      The connection alias or the desired
     *                                      connection's global instance index
     * @return  PDODB_Connection            The database connection instance
     * @throws  PDORM_InstanceNotFoundException     If the specified alias
     *                                              is not associated with a
     *                                              connection instance
     * @access  public
     * @static
     */
    public static function getConnection($alias)
    {
        $inst = self::getInstance();

        if (is_string($alias) && isset($inst->connections[$alias])) {
            $alias = $inst->connections[$alias];
        }

        return PDODB::getConnection($alias);
    }

}

/**
 * Asserts that each key in the array is also present in valid_keys. Throws
 * an exception if array contains an invalid key.
 *
 * @param   array   $array          The array to check
 * @param   array   $valid_keys     An array containing valid key names
 * @return  void
 * @throws  Exception               If there are invalid keys
 * @access  public
 */
function PDORM_assert_valid_keys($array, $valid_keys)
{
    $invalid_keys = array();
    foreach (array_keys($array) as $key) {
        if (!in_array($key, $valid_keys)) {
            $invalid_keys[] = $key;
        }
    }

    if (count($invalid_keys) > 0) {
        throw new Exception('Invalid key(s): ' . implode(', ', $invalid_keys));
    }
}

/**
 * Tells whether an array is associative or not. In order to be non-associative,
 * each of the array's key numbers must correspond exactly to it's position
 * in the array.
 *
 * @param   array   $array      The array to check
 * @return  bool                True if the array is associative
 * @access  public
 */
function PDORM_is_assoc($array)
{
    return is_array($array) && count($array) !== array_reduce(array_keys($array),
        'PDORM_is_assoc_callback', 0);
}

/**
 * @ignore
 */
function PDORM_is_assoc_callback($a, $b)
{
    return $a === $b ? $a + 1 : 0;
}

/**
 * Converts the first character in a string to lowercase.
 *
 * @param   string  $str        The string to convert
 * @return  string              The string with the first letter lowercased
 * @access  public
 */
function PDORM_lcfirst($str)
{
    return strtolower(substr($str, 0, 1)) . substr($str, 1);
}

/**
 * Converts a camel cased string to an underscore separated one. Example:
 * someCamelCasedString => some_camel_cased_string
 *
 * @param   string  $camel      The string to convert
 * @return  string              The string separated with underscores
 * @access  public
 */
function PDORM_camel2uscore($str)
{
    return strtolower(preg_replace('/([a-z])([A-Z])/', '\1_\2', $str));
}

/**
 * Converts an underscore separated string to a camel cased one. Example:
 * some_under_scored_string => someUnderScoredString
 *
 * @param   string  $underscore     The string to convert
 * @return  string                  The string camel-cased
 * @access  public
 */
function PDORM_uscore2camel($str)
{
    return PDORM_lcfirst(preg_replace('/(_[a-z])/e', "strtoupper('\\1')", $str));
}

?>
